Skip to content

Add Google OAuth login and GAR CI/CD pipeline#1

Merged
dnplkndll merged 2 commits intomasterfrom
feature/google-oauth-login
Feb 28, 2026
Merged

Add Google OAuth login and GAR CI/CD pipeline#1
dnplkndll merged 2 commits intomasterfrom
feature/google-oauth-login

Conversation

@dnplkndll
Copy link
Copy Markdown

@dnplkndll dnplkndll commented Feb 15, 2026

Summary

  • Enable Google OAuth2 sign-in for self-hosted DocuSeal (gated on GOOGLE_CLIENT_ID env var)
  • Add GitHub Actions workflow to build linux/amd64 images and push to Google Artifact Registry
  • GCP infrastructure (WIF pool, SA, AR repo) has been provisioned

Image

us-central1-docker.pkg.dev/kencove-prod/docuseal/docuseal

Tags: semver (1.2.3), short SHA (abc1234), latest (on default branch)

Google OAuth Changes

File Change
Gemfile Added omniauth-google-oauth2, omniauth-rails_csrf_protection
app/models/user.rb Conditional :omniauthable devise module
config/initializers/devise.rb google_oauth2 provider config with hd: kencove.com
config/routes.rb Added omniauth_callbacks to devise routes
app/controllers/omniauth_callbacks_controller.rb New — OAuth callback handler
lib/users.rb New — OAuth user lookup (find-only, no auto-create)
config/locales/i18n.yml Added error message i18n keys

GAR CI/CD Workflow

.github/workflows/docker-gar.yml — triggers on semver tags + feature/google-oauth-login branch.

GCP Resources Created

Resource Details
Artifact Registry us-central1-docker.pkg.dev/kencove-prod/docuseal (docker)
WIF Pool projects/103143301688/locations/global/workloadIdentityPools/github-actions
OIDC Provider github — issuer token.actions.githubusercontent.com, scoped to kencove org
Service Account github-actions-docuseal@kencove-prod.iam.gserviceaccount.com
IAM SA has artifactregistry.writer on docuseal repo; WIF binding scoped to kencove/docuseal
GitHub Secrets GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_SERVICE_ACCOUNT set on repo

Environment Variables for Deployment

To enable Google OAuth in the cluster, set these on the DocuSeal deployment:

env:
  - name: GOOGLE_CLIENT_ID
    value: "<your-google-oauth-client-id>"
  - name: GOOGLE_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: docuseal-google-oauth
        key: client-secret

Google OAuth App Setup (Google Cloud Console)

  1. Go to APIs & Services → Credentials in the kencove-prod GCP project
  2. Create an OAuth 2.0 Client ID (Web application type)
  3. Set Authorized redirect URI to: https://<your-docuseal-domain>/users/auth/google_oauth2/callback
  4. Copy the Client ID and Client Secret into the K8s env/secret above

Behavior

  • With env vars set: Google "Sign in with Google" button appears on /sign_in, restricted to @kencove.com accounts
  • Without env vars: Standard email/password login only (no change)
  • Users must be pre-created by an admin — Google login only matches existing users, no self-registration

Test plan

  • GAR workflow triggers on push to feature/google-oauth-login
  • Image appears at us-central1-docker.pkg.dev/kencove-prod/docuseal/docuseal
  • App starts without errors with GOOGLE_CLIENT_ID set
  • App starts without errors without GOOGLE_CLIENT_ID (no regression)
  • Google sign-in button visible when env vars present
  • Sign-in redirects to Google, callback signs in existing user
  • Non-existent user gets "User not found" error

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Google OAuth2 login option (domain-restricted) and user lookup; shows clear messages for authentication failures and missing users.
  • Chores

    • Added OAuth-related dependencies to support Google sign-in.
    • Added CI workflow to build and push Docker images to cloud artifact registry on semantic-versioned tags.

Enable Google OAuth2 login for self-hosted DocuSeal when GOOGLE_CLIENT_ID
env var is present. Adds GitHub Actions workflow to build amd64 images and
push to Google Artifact Registry via Workload Identity Federation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

Adds conditional Google OAuth2 (OmniAuth + Devise) sign-in, a Users.from_omniauth lookup helper, related routes/locale entries, two gems, and a GitHub Actions workflow to build and push Docker images to Google Artifact Registry on semantic version tags.

Changes

Cohort / File(s) Summary
CI/CD Pipeline
​.github/workflows/docker-gar.yml
New GitHub Actions workflow that builds and pushes Docker images to Google Artifact Registry on tag pushes matching v*.*.*; includes GCP workload identity auth, access token login to GAR, Buildx build-and-push with multiple tags and caching.
Dependencies
Gemfile
Adds omniauth-google-oauth2 and omniauth-rails_csrf_protection gems to enable Google OAuth2 and CSRF protection for OmniAuth.
Devise configuration & translations
config/initializers/devise.rb, config/routes.rb, config/locales/i18n.yml
Conditionally configures Devise OmniAuth for Google when GOOGLE_CLIENT_ID is present (client ID/secret and hosted domain); adjusts devise_for routes to include :omniauth_callbacks conditionally; adds user_not_found and authentication_failed i18n keys.
Authentication logic
app/controllers/omniauth_callbacks_controller.rb, app/models/user.rb, lib/users.rb
Adds OmniauthCallbacksController (handles google_oauth2 and failure), conditionally enables :omniauthable in User when GOOGLE_CLIENT_ID exists, and adds Users.from_omniauth(oauth) helper to find users by normalized email.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Browser
    participant Rails as Rails App
    participant Google as Google OAuth
    participant DB as Database

    User->>Rails: Click "Sign in with Google"
    Rails->>Google: Redirect to OAuth authorization
    Google->>User: Show consent screen
    User->>Google: Authorize
    Google->>Rails: Redirect with auth code
    Rails->>Google: Exchange code for access token
    Google->>Rails: Return access token & user info
    Rails->>Rails: OmniauthCallbacksController#google_oauth2
    Rails->>DB: Users.from_omniauth(email)
    DB->>Rails: Return user record or nil
    alt User found and active
        Rails->>Rails: Sign in user
        Rails->>User: Redirect to dashboard
    else User not found or inactive
        Rails->>User: Redirect to login with alert
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
I hopped through code with glee tonight,
Google keys and callbacks bright,
Devise and OmniAuth in tune,
New sign-ins under the moon,
Hooray — containers take flight! 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: adding Google OAuth login functionality and implementing a CI/CD pipeline for Google Artifact Registry.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/google-oauth-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @.github/workflows/docker-gar.yml:
- Around line 25-31: The "Create .version file" run block uses ${{
github.ref_name }} directly, which can be an injection vector; instead, add an
environment variable (e.g., REF_NAME) in that job/step set to ${{
github.ref_name }} and reference the shell-safe variable (e.g., "$REF_NAME")
inside the run script (keep the existing github.ref_type check and fallback to
github.sha as an env var if desired) and ensure the variable is quoted when
writing to .version to avoid word-splitting or shell expansion.
- Around line 3-8: Remove the development branch trigger under on.push.branches
(delete the "feature/google-oauth-login" entry) and tighten the tag trigger
under on.push.tags from "*.*.*" to a semver-style pattern such as "v*.*.*" (or
use pattern={{version}} with docker/metadata-action) so only proper semver tags
trigger the workflow; ensure any downstream uses of the tag
(docker/metadata-action inputs) are updated to match the chosen convention.

In `@app/models/user.rb`:
- Line 72: The app boots fail because the User model conditionally includes
devise :omniauthable while routes unconditionally map omniauth_callbacks; make
the guard consistent by introducing a shared early-loaded flag (e.g.
GOOGLE_OAUTH_ENABLED) and use it in both the model line that currently reads
devise :omniauthable, omniauth_providers: [:google_oauth2] and in the routes
where omniauth_callbacks is added (or alternatively wrap the routes'
omniauth_callbacks mapping in the same ENV check); ensure the initializer that
sets GOOGLE_OAUTH_ENABLED is loaded before routes and models so both sides see
the same boolean.

In `@config/locales/i18n.yml`:
- Around line 176-177: The new i18n keys user_not_found and
authentication_failed were added only for English; update all supported locale
YAML files to include equivalent translations for these keys (or add them to the
locale fallback file) so missing-translation errors don't occur; locate the
locale files that mirror i18n.yml (e.g., the other locale YAMLs or the default
fallback locale) and add matching keys with appropriate translated messages, or
confirm/update the app's I18n.fallbacks configuration to ensure these keys
resolve to the English strings when a locale translation is absent.

In `@config/routes.rb`:
- Around line 17-19: The devise_for :users route registers omniauth_callbacks
unconditionally causing boot failure when User is not omniauthable; update the
routes so omniauth_callbacks is only added when ENV['GOOGLE_CLIENT_ID'] (the
same guard used in the User model) is present — conditionally include the
:omniauth_callbacks controller and the :omniauthable routes in the devise_for
:users call (or split into two devise_for calls) so that omniauth_callbacks is
not registered unless ENV['GOOGLE_CLIENT_ID'] is set.
🧹 Nitpick comments (2)
config/initializers/devise.rb (1)

337-342: Consider making the hosted domain configurable via an environment variable.

The hd: 'kencove.com' restriction is hardcoded. If this app is intended to be a reusable self-hosted deployment, extracting it to an env var (e.g., GOOGLE_HOSTED_DOMAIN) would make it more flexible without code changes.

Suggested change
   if ENV['GOOGLE_CLIENT_ID'].present?
     config.omniauth :google_oauth2,
                     ENV.fetch('GOOGLE_CLIENT_ID'),
                     ENV.fetch('GOOGLE_CLIENT_SECRET'),
-                    { hd: 'kencove.com' }
+                    { hd: ENV.fetch('GOOGLE_HOSTED_DOMAIN', 'kencove.com') }
   end
app/controllers/omniauth_callbacks_controller.rb (1)

6-14: Consider distinguishing "user not found" from "user archived/inactive".

Both a missing user (nil) and an archived/locked user (active_for_authentication? == false) produce the same user_not_found flash message. This can be confusing for an admin debugging login issues for a known but deactivated user.

A small improvement:

💡 Suggested change
   def google_oauth2
     user = Users.from_omniauth(request.env['omniauth.auth'])
 
     if user&.active_for_authentication?
       sign_in_and_redirect(user, event: :authentication)
+    elsif user
+      redirect_to new_user_session_path, alert: I18n.t('authentication_failed')
     else
       redirect_to new_user_session_path, alert: I18n.t('user_not_found')
     end
   end

Comment on lines +176 to +177
user_not_found: User not found. Please contact your administrator.
authentication_failed: Authentication failed. Please try again.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add translations for non‑English locales.

These new keys are only present in en. If other locales are active, they’ll render as missing translations. Please add equivalent keys for each supported locale or confirm a fallback strategy.

🤖 Prompt for AI Agents
In `@config/locales/i18n.yml` around lines 176 - 177, The new i18n keys
user_not_found and authentication_failed were added only for English; update all
supported locale YAML files to include equivalent translations for these keys
(or add them to the locale fallback file) so missing-translation errors don't
occur; locate the locale files that mirror i18n.yml (e.g., the other locale
YAMLs or the default fallback locale) and add matching keys with appropriate
translated messages, or confirm/update the app's I18n.fallbacks configuration to
ensure these keys resolve to the English strings when a locale translation is
absent.

- Make omniauth_callbacks routes conditional on GOOGLE_CLIENT_ID to
  prevent boot failure when the env var is missing
- Fix shell injection in docker-gar.yml by using env vars instead of
  direct GitHub expression interpolation in run steps
- Remove feature branch trigger and tighten tag pattern to v*.*.*
- Make Google hosted domain configurable via GOOGLE_HOSTED_DOMAIN env var

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/docker-gar.yml:
- Around line 55-58: The workflow's tags block uses
"type=raw,value=latest,enable={{is_default_branch}}" which prevents the latest
tag from being applied on tag-triggered runs (github.ref for tags isn't the
default branch); update the docker/metadata-action configuration so latest is
applied for semver/tag pushes—either replace the conditional with the action's
automatic handling by adding "flavor: latest=auto" to the metadata-action
invocation, or make the latest tag unconditional by using
"type=raw,value=latest", or remove the latest line if not desired; edit the tags
block (the line containing type=raw,value=latest and the
enable={{is_default_branch}}) or add the flavor setting to implement your chosen
approach.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa2031 and a100f6a.

📒 Files selected for processing (3)
  • .github/workflows/docker-gar.yml
  • config/initializers/devise.rb
  • config/routes.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/initializers/devise.rb

Comment on lines +55 to +58
tags: |
type=semver,pattern={{version}}
type=sha,prefix=,format=short
type=raw,value=latest,enable={{is_default_branch}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

docker metadata-action is_default_branch template variable tag push behavior

💡 Result:

In docker/metadata-action, {{is_default_branch}} is only about the branch ref that triggered the workflow run (i.e., a refs/heads/... run). It returns true only when the workflow was triggered by the repo’s default branch; otherwise false. (github.com)

That means on a tag push run (refs/tags/...), there is no triggering branch ref (e.g., {{branch}} is empty on tag runs), so {{is_default_branch}} is not a reliable way to detect “tag created from default branch”. (github.com)

If your goal is “push latest only for default branch builds”:

  • Do it on branch push runs with a conditional raw tag, e.g. type=raw,value=latest,enable={{is_default_branch}}. (github.com)

If your goal is “push latest on release tags”:

  • Rely on the action’s built-in latest tag handling (flavor: latest=auto by default), which generates latest for tag/semver/match-style tag strategies. (github.com)

Finally, while {{base_ref}} may sometimes show something on tag push payloads, the action explicitly warns it doesn’t always return the expected branch on tag pushes and is not recommended to rely on for this purpose. (github.com)

Citations:


The latest tag may never be applied on tag-triggered workflows.

When this workflow is triggered by a tag push, github.ref is refs/tags/v*.*.*, not a branch ref. The is_default_branch template in docker/metadata-action evaluates whether the current ref is the default branch—which is false for tag pushes. This means the latest tag will never be applied.

If you want latest to be applied when tagging, consider one of these approaches:

  1. Use the action's built-in latest tag handling by setting flavor: latest=auto (applies latest for tag/semver tag strategies)
  2. Always apply latest on semver tags: type=raw,value=latest
  3. Remove the latest tag line if it's not needed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/docker-gar.yml around lines 55 - 58, The workflow's tags
block uses "type=raw,value=latest,enable={{is_default_branch}}" which prevents
the latest tag from being applied on tag-triggered runs (github.ref for tags
isn't the default branch); update the docker/metadata-action configuration so
latest is applied for semver/tag pushes—either replace the conditional with the
action's automatic handling by adding "flavor: latest=auto" to the
metadata-action invocation, or make the latest tag unconditional by using
"type=raw,value=latest", or remove the latest line if not desired; edit the tags
block (the line containing type=raw,value=latest and the
enable={{is_default_branch}}) or add the flavor setting to implement your chosen
approach.

@dnplkndll dnplkndll merged commit ef4a99e into master Feb 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant